package org.fjsei.yewu.graphql;

import graphql.PublicApi;
import graphql.relay.*;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.util.Assert;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import static graphql.Assert.assertTrue;
import static java.lang.String.format;
import static java.util.Base64.getDecoder;
import static java.util.Base64.getEncoder;

/**Relay包装器功能
 * ES searchAfter,或者 Zeebe的TaskList:Appolo3分页能力提供的 分页查询。
 * 前置条件：采取数据库分页后的场景。 无需要知道潜在的列表items[]的大小;
 * 数据库可支持任意大小的集合。最简单的小规模数据集请用MemoryListConnection；
 * Relay标准输入要用after,before,first,last 来。
 * 注意first配套after；last配套before；不能交叉配套。
 * Relay标准输出：
 * connection集合代理{edges【{cursor node}】; pageInfo{ hasPreviousPage  hasNextPage startCursor and endCursor}}
 * */
@PublicApi
public class AfterValuesConnection<T extends AfterValues> extends MemoryListConnection<T> {
    //search after或者zeebe TaskList分页的当前游标数组。从DataFetchingEnvironment提取出。
    private List<String>  cursorValues;
    //    private int offset;
    //向前查，若是比起limit还少就是到结尾了；
    //若倒着向后查的：若是offset比last|maxLimit小的，就是到了顶部。
    private boolean onlistTop=false;
    private boolean back=false;


    public AfterValuesConnection(String prefix, int maxLimit, DataFetchingEnvironment env) {
        assertTrue(prefix != null && !prefix.isEmpty(), () -> "prefix cannot be null or empty");
        this.prefix = prefix;
        this.limit=maxLimit;

        List<String>  afterOffset = getOffsetFromCursor(env.getArgument("after"));
        List<String>  beforeOffset = getOffsetFromCursor(env.getArgument("before"));

        Integer first = env.getArgument("first");
        Integer last = env.getArgument("last");
        //last配套before;
        if(last!=null && beforeOffset!=null)
        {
            back=true;
            cursorValues= beforeOffset;
            limit=Math.min(last!=null? last:3, maxLimit);
        }else{
            //first配套after； 缺省是first=top;
            cursorValues= afterOffset;
            //默认3条
            limit=Math.min(first!=null? first:3, maxLimit);
        }
        //assertTrue(limit<=maxLimit, () -> format("limit cannot > %d", maxLimit) );
    }

    public AfterValuesConnection(DataFetchingEnvironment env) {
        this(DUMMY_CURSOR_PREFIX,20,env);
    }
    /**滚动方向
     * */
    public boolean isBack() {
        return back;
    }
    /**对应的 search After分页模式
     * @return 解包之后的[]
     * */
    public List<String> getCursorValues() {
        return cursorValues;
    }

    /**这里特别：
     * edges是单页面的，并非说的是全部的列表。PageInfo计算不一样了
     * */
    @Override
    public Connection<T> get(DataFetchingEnvironment environment) {
        List<Edge<T>> edges = buildEdges();
        if (edges.size() == 0) {
            return emptyConnection();
        }
        //注意first配套after；实际返回edge数量达不到first|limit代表走到最后了。
        //last配套before；实际返回edge数量达不到last|limit代表跑到最前头了。
        Edge<T> firstEdge = edges.get(0);
        Edge<T> lastEdge = edges.get(edges.size() - 1);
        PageInfo pageInfo = new DefaultPageInfo(
                firstEdge.getCursor(),
                lastEdge.getCursor(),
                !onlistTop,
                edges.size()>=limit
        );
        return new DefaultConnection<>(
                edges,
                pageInfo
        );
    }
    /**依据sortValues[String,] 打包游标取值：而不是offset顺序号。
     * 后续前端loadNext()请求处理时刻，后端需要依据还原的sortValues/afterValues来定位上一次的取值来分页滚动。
     * */
    protected List<Edge<T>> buildEdges() {
        List<Edge<T>> edges = new ArrayList<>();
        //数据库分页每次只是取到单页面数据才需要设置wholeOffset初始位置。
        for (T object : data) {
            edges.add(new DefaultEdge<>(object, new DefaultConnectionCursor(createCursor(object.getAfterValues()))));
        }
        return edges;
    }
    /*依赖cursor来接续分页
    * */
    protected String createCursor(List<String>  afterValues) {
        assertTrue( afterValues!=null, () -> "afterValues空的");
        int items= afterValues.size();
        final char splchr = '☏';    //分割不混淆。
        StringBuilder  builder = new StringBuilder(128);
        for (int i = 0; i < items; i++) {
            if(i>0)
                builder.append(splchr).append(afterValues.get(i));
            else
                builder.append(afterValues.get(i));
        }
        String cursor=builder.toString();
        assertTrue( !cursor.isEmpty(), () -> "afterValues空的");
        byte[] bytes =cursor.getBytes(StandardCharsets.UTF_8);
        return getEncoder().encodeToString(bytes);
    }
    /**解包createCursor生成的串：游标构成的每一个排序定位字段;
     * 浏览器无状态的：两次请求包依赖cursor来接续分页的定位描述。
     * */
    protected List<String>  getOffsetFromCursor(String cursor) {
        if (cursor == null) {
            return null;
        }
        byte[] decode;
        try {
            decode = getDecoder().decode(cursor);
        } catch (IllegalArgumentException e) {
            throw new InvalidException(format("The cursor is not in base64 format : '%s'", cursor), e);
        }
        String cursorDc = new String(decode, StandardCharsets.UTF_8);
        final String splchr = String.valueOf('☏');    //分割不混淆。
        String[] splits = cursorDc.split(splchr);        //相反的 String.join("-", "Java",。。)
        return List.of(splits);
    }
}

